package com.dbexposer.db;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.sql.DataSource;

public class DatabaseType {

	private static final ArrayList<DatabaseType> TYPES;

	static {

		try {
			TYPES = new ArrayList<DatabaseType>();
			Properties typesProperties = new Properties();
			String[] drivers = null;

			typesProperties.load(DatabaseType.class.getClassLoader().getResourceAsStream("db/databasetypes.properties"));
			
			drivers = typesProperties.getProperty("driverIds").split(";");

			// Create DatabaseType list
			DatabaseType type = null;
			for (String driver : drivers) {

				if(driver.isEmpty()) {
					continue;
				}
				
				type = new DatabaseType(driver, typesProperties);

				TYPES.add(type);
			}
			
		} catch (IOException e) {
			throw new RuntimeException("Unable to load database types properties", e);
		}

	}

	public static List<DatabaseType> getDatabaseTypes() {
		return TYPES;
	}
	
	public static DatabaseType getDatabaseTypeByDriverId(String driverId) {
		
		for (DatabaseType type : TYPES) {
			if(type.driverId.equals(driverId)) {
				return type;
			}
		}
		
		return null;
	}

	private String driverId;
	private final String dataSourceClass;
	private final String dataSourceConfig;
	private final String driverClass;
	private final String dbVersions;
	private final String dbManufacturer;
	private final String urlFormat;

	private DatabaseType(String driverId, Properties typesProperties) {
		
		this.driverId = driverId;
		this.dataSourceClass = typesProperties.getProperty(driverId + ".datasource.class").trim();
		this.dataSourceConfig = typesProperties.getProperty(driverId + ".datasource.config").trim();
		this.driverClass = typesProperties.getProperty(driverId + ".driver.class").trim();
		this.dbVersions = typesProperties.getProperty(driverId + ".driver.db.versions").trim();
		this.dbManufacturer = typesProperties.getProperty(driverId + ".driver.db.manufacturer").trim();
		this.urlFormat = typesProperties.getProperty(driverId + ".url.format").trim();

	}
	
	public DataSource createDataSource(String url, String user, String password) {

		DataSource dataSource = null;
		
		try {
			dataSource = (DataSource) this.getClass().getClassLoader().loadClass(dataSourceClass).newInstance();
			
			configureDataSource(dataSource, url, user, password);
			
			return dataSource;
			
		} catch (Exception e) {
			throw new IllegalStateException("Error creating data source", e);
		}
		
	}

	private void configureDataSource(DataSource dataSource, String url, String user, String password) throws Exception {

		DatabaseTypeVarContext ctx = new DatabaseTypeVarContext(this, url, user, password);
		String[] configSteps = dataSourceConfig.split(";");
		
		for (String configStep : configSteps) {
			executeDataSourceConfigStep(configStep, dataSource, ctx);
		}
		
	}

	private void executeDataSourceConfigStep(String configStep, DataSource dataSource, 
			DatabaseTypeVarContext ctx) throws Exception {

		Class<? extends DataSource> dataSourceClass = dataSource.getClass();
		
		String methodName = getConfigMethodName(configStep);
		Class[] paramTypes = getConfigMethodParamTypes(configStep, ctx);
		Object[] methodArgs = getConfigMethodArgs(configStep, ctx);
		
		Method configMethod = dataSourceClass.getMethod(methodName, paramTypes);
		
		configMethod.invoke(dataSource, methodArgs);
	}
	
	private String getConfigMethodName(String configStep) {
		return configStep.substring(0, configStep.indexOf('('));
	}
	
	private Class[] getConfigMethodParamTypes(String configStep, DatabaseTypeVarContext ctx) {
		
		List<Class> paramTypes = new ArrayList<Class>();
		String[] vars = configStep.substring(configStep.indexOf('(') + 1, configStep.indexOf(')')).split(",");
		
		for (String var : vars) {
			paramTypes.add(ctx.getVarType(var));
		}
		
		return paramTypes.toArray(new Class[0]);
	}
	
	private Object[] getConfigMethodArgs(String configStep, DatabaseTypeVarContext ctx) {
		
		List<Object> args = new ArrayList<Object>();
		String[] vars = configStep.substring(configStep.indexOf('(') + 1, configStep.indexOf(')')).split(",");

		for (String var : vars) {
			
			if(ctx.getVarType(var) == Integer.class) {
				args.add(ctx.getIntVar(var));
			} else {
				args.add(ctx.getStrVar(var));
			}
		}
		
		return args.toArray();
	}

	public String getDataSourceClass() {
		return dataSourceClass;
	}

	public String getDriverClass() {
		return driverClass;
	}

	public String getDbVersions() {
		return dbVersions;
	}

	public String getDbManufacturer() {
		return dbManufacturer;
	}

	public String getUrlFormat() {
		return urlFormat;
	}

}
